﻿using OpenSource.UPnP;
using System;
using System.Xml;
using MediaServer.Utilities;
using System.Collections.Generic;

namespace MediaServer
{
    /// <summary>
    /// ContentDirectory service provides a uniform mechanism for UI devices
    /// to browse the content on the server and to obtain detailed information
    //  about individual content objects.
    /// </summary>
    public class DeviceContentDirectoryService : GenericContentDirectoryService
    {
        private ILogger logger;
        private VirtualDirectory root;
        
        private void log(string message)
        {
            if (this.logger != null)
            {
                this.logger.log(String.Format("({0}): {1}", this.GetCaller().ToString(), message));
            }
        }

        public DeviceContentDirectoryService(ILogger logger, VirtualDirectory root)
            : base()
        {
            this.logger = logger;
            this.root = root;
        }

        /// <summary>
        /// Action: Browse
        /// </summary>
        /// <param name="ObjectID">Associated State Variable: A_ARG_TYPE_ObjectID\n
        /// The @id of the object currently being browsed. An ObjectID value of zero
        /// corresponds to the root object of the ContentDirectory service.
        /// </param>
        /// <param name="BrowseFlag">Associated State Variable: A_ARG_TYPE_BrowseFlag\n
        /// This REQUIRED state variable is introduced to provide type information for the BrowseFlag argument in
        /// the Browse() action. A BrowseFlag argument specifies a browse option to be used for browsing the
        /// ContentDirectory service.\n\n
        /// Valid values are:\n
        /// "BrowseMetadata" - this indicates that the properties of the object specified by the ObjectID
        /// argument will be returned in the Result argument.\n
        /// "BrowseDirectChildren" - this indicates that first level objects under the object specified by the ObjectID
        /// argument will be returned in the Result argument, as well as the metadata of all objects specified.
        /// </param>
        /// <param name="Filter">Associated State Variable: A_ARG_TYPE_Filter\n
        /// This REQUIRED state variable is introduced to provide type information for the Filter argument in the
        /// Browse() and Search() actions. The comma-separated list of property specifiers indicates which metadata
        /// properties are to be returned in the Result of the Browse() or Search() actions. Each property name MUST
        /// include the standard namespace prefix for that property, except for the DIDL-Lite namespace. Properties in
        /// the DIDL-Lite namespace MUST always be returned without the prefix. All property names MUST be
        /// fully qualified using the double colon ("::") syntax as defined in Section 2.2.20, "CDS Properties". 
        /// \n\nFor example, "upnp:foreignMetadata::fmBody::fmURI".
        /// </param>
        /// <param name="StartingIndex">Associated State Variable: A_ARG_TYPE_Index\n
        /// Zero-based offset to enumerate children under the container specified by ObjectID.
        /// StartingIndex MUST be set to 0 if BrowseFlag is equal to "BrowseMetadata".
        /// </param>
        /// <param name="RequestedCount">Associated State Variable: A_ARG_TYPE_Count\n
        /// Requested number of entries under the object specified by ObjectID.
        /// RequestedCount = 0 indicates request all entries.
        /// </param>
        /// <param name="SortCriteria">Associated State Variable: A_ARG_TYPE_SortCriteria\n
        /// This REQUIRED state variable is introduced to provide type information for the SortCriteria argument in
        /// the Browse() and Search() actions. A_ARG_TYPE_SortCriteria is a possibly empty CSV list of property
        /// names, each of which MUST be prefixed by a sort modifier. Each property name MUST include the
        /// standard namespace prefix for that property, except for the DIDL-Lite namespace. Properties in the DIDLLite
        /// namespace MUST always be returned without the prefix. All property names MUST be fully qualified
        /// using the double colon ("::") syntax as defined in Section 2.2.20, "CDS Properties".
        /// \n\nFor example, “upnp:foreignMetadata::fmBody::fmURI”.
        /// </param>
        /// <param name="Result">Associated State Variable: A_ARG_TYPE_Result\n
        /// This REQUIRED state variable is introduced to provide type information for the Result argument in
        /// various actions. The structure of the Result argument is a DIDL-Lite XML Document.
        /// </param>
        /// <param name="NumberReturned">Associated State Variable: A_ARG_TYPE_Count\n
        /// Number of objects returned in the Result argument. If BrowseFlag is set to
        /// "BrowseMetadata", then NumberReturned MUST be set to 1.
        /// </param>
        /// <param name="TotalMatches">Associated State Variable: A_ARG_TYPE_Count\n
        /// Total number of ContentDirectory service objects that match the search criteria
        /// (specified by the SearchCriteria argument, and independent of the starting index specified by the
        /// StartingIndex argument) under the object specified by the ContainerID argument.
        /// \n\n
        /// If BrowseFlag is set to "BrowseMetadata", then TotalMatches MUST be set to 1.
        /// Else if BrowseFlag is set to "BrowseDirectChildren", then TotalMatches MUST be set to the total
        /// number of objects in the object specified for the Browse() action (independent of the starting index
        /// specified by the StartingIndex argument).
        /// \n\n
        /// If the ContentDirectory service implementation cannot timely compute the value of TotalMatches,
        /// but there are matching objects that have been found by the ContentDirectory service
        /// implementation, then the Browse() action MUST successfully return with the TotalMatches
        /// argument set to zero and the NumberReturned argument indicating the number of returned objects.\n
        /// If the ContentDirectory service implementation cannot timely compute the value of TotalMatches,
        /// and there are no matching objects found, then the Browse() action MUST return error code 720.
        /// </param>
        /// <param name="UpdateID">Associated State Variable: A_ARG_TYPE_UpdateID\n
        /// The value returned in the UpdateID argument MUST be the SystemUpdateID state
        /// variable value at the time the Browse() response was generated. If a control point finds that the
        /// current SystemUpdateID state variable value is not equal to the value returned in the UpdateID
        /// argument, then a change within the ContentDirectory service has occurred between the time the
        /// result was generated and the time that the control point is processing the result. The control point
        /// may therefore want to re-invoke the Browse() action to ensure that it has the latest property values.
        /// Note however that the change in the value of the SystemUpdateID state variable may have been
        /// caused by a change that occurred in a location in the ContentDirectory tree hierarchy that is not
        /// part of the returned result. In this case, the re-invocation of the Browse() action will return the
        /// exact same result.
        /// </param>
        /// <remarks>
        /// This REQUIRED action allows the caller to incrementally browse the native hierarchy of the
        /// ContentDirectory service objects exposed by the ContentDirectory service, including information listing
        /// the classes of objects available in any particular object container.
        /// </remarks>
        public override void Browse(System.String ObjectID, Enum_A_ARG_TYPE_BrowseFlag BrowseFlag, System.String Filter, System.UInt32 StartingIndex, System.UInt32 RequestedCount, System.String SortCriteria, out System.String Result, out System.UInt32 NumberReturned, out System.UInt32 TotalMatches, out System.UInt32 UpdateID)
        {
            //TODO: BrowseFlag - metadata option support, SortCriteria support

            this.log("ContentDirectory.Browse(" + ObjectID.ToString() + " " + BrowseFlag.ToString() + " " + Filter.ToString() + " " + StartingIndex.ToString() + " " + RequestedCount.ToString() + " " + SortCriteria.ToString() + ")");

            IDIDLSerializable obj = this.root.Find(ObjectID);
            if (obj != null)
            {
                List<IDIDLSerializable> raw = obj.Browse();

                XmlDocument doc = new XmlDocument();
                XmlElement didl = (XmlElement)doc.AppendChild(doc.CreateElement("DIDL-Lite"));
                didl.SetAttribute("xmlns", "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/");
                didl.SetAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1/");
                didl.SetAttribute("xmlns:upnp", "urn:schemas-upnp-org:metadata-1-0/upnp/");
                didl.SetAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0/");

                if (RequestedCount == 0)
                {
                    RequestedCount = (uint)raw.Count;
                }

                foreach(IDIDLSerializable res in raw.GetRange((int)StartingIndex, (int)RequestedCount)) 
                {
                    didl.AppendChild(res.SingleXML(doc));
                }

                TotalMatches = (uint)raw.Count;
                NumberReturned = (uint)didl.ChildNodes.Count;

                Result = doc.OuterXml;

                UpdateID = 0;  // TODO
            }
            else
            {
                // Specification orders throwing error 701 "No such object".
                throw new UPnPCustomException(701, "No such object");
            }
        }

        /// <summary>
        /// Action: Search
        /// </summary>
        /// <param name="ContainerID">Associated State Variable: A_ARG_TYPE_ObjectID</param>
        /// <param name="SearchCriteria">Associated State Variable: A_ARG_TYPE_SearchCriteria</param>
        /// <param name="Filter">Associated State Variable: A_ARG_TYPE_Filter</param>
        /// <param name="StartingIndex">Associated State Variable: A_ARG_TYPE_Index</param>
        /// <param name="RequestedCount">Associated State Variable: A_ARG_TYPE_Count</param>
        /// <param name="SortCriteria">Associated State Variable: A_ARG_TYPE_SortCriteria</param>
        /// <param name="Result">Associated State Variable: A_ARG_TYPE_Result</param>
        /// <param name="NumberReturned">Associated State Variable: A_ARG_TYPE_Count</param>
        /// <param name="TotalMatches">Associated State Variable: A_ARG_TYPE_Count</param>
        /// <param name="UpdateID">Associated State Variable: A_ARG_TYPE_UpdateID</param>
        public override void Search(System.String ContainerID, System.String SearchCriteria, System.String Filter, System.UInt32 StartingIndex, System.UInt32 RequestedCount, System.String SortCriteria, out System.String Result, out System.UInt32 NumberReturned, out System.UInt32 TotalMatches, out System.UInt32 UpdateID)
        {
            this.log("Not implemented: ContentDirectoryService_Search(" + ContainerID.ToString() + SearchCriteria.ToString() + Filter.ToString() + StartingIndex.ToString() + RequestedCount.ToString() + SortCriteria.ToString() + ")");

            if (SearchCriteria.StartsWith("dc:title"))
            {
                IDIDLSerializable obj = this.root.Find(ContainerID);
                if (obj is SharedDirectory)
                {
                    List<IDIDLSerializable> raw = ((SharedDirectory)obj).Search(SearchCriteria);

                    XmlDocument doc = new XmlDocument();
                    XmlElement didl = (XmlElement)doc.AppendChild(doc.CreateElement("DIDL-Lite"));
                    didl.SetAttribute("xmlns", "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/");
                    didl.SetAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1/");
                    didl.SetAttribute("xmlns:upnp", "urn:schemas-upnp-org:metadata-1-0/upnp/");
                    didl.SetAttribute("xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0/");

                    if (RequestedCount == 0)
                    {
                        RequestedCount = (uint)raw.Count;
                    }

                    foreach (IDIDLSerializable res in raw.GetRange((int)StartingIndex, (int)RequestedCount))
                    {
                        didl.AppendChild(res.SingleXML(doc));
                    }

                    TotalMatches = (uint)raw.Count;
                    NumberReturned = (uint)didl.ChildNodes.Count;

                    Result = doc.OuterXml;

                    UpdateID = 0;
                }
                else
                {
                    throw new UPnPCustomException(701, "No such object");
                }
            }
            else
            {
                throw new UPnPCustomException(708, "Unsupported or invalid search criteria");
            }
        }

        /// <summary>
        /// Action: GetSearchCapabilities
        /// </summary>
        /// <param name="SearchCaps">Associated State Variable: SearchCapabilities</param>
        /// <remarks>
        /// This REQUIRED action returns the searching capabilities that are supported by the device.
        /// 
        /// When it connects to the ContentDirectory service, the control point determines which properties can be used
        /// in the SearchCriteria argument of the Search() action. It does this via the GetSearchCapabilities() action:
        /// 
        /// Request:
        /// GetSearchCapabilities()
        /// 
        /// Response:
        /// GetSearchCapabilities("dc:title,dc:creator,dc:date,upnp:class,res@size")
        /// </remarks>
        public override void GetSearchCapabilities(out System.String SearchCaps)
        {
            SearchCaps = "dc:title";
        }

        /// <summary>
        /// Action: GetSortCapabilities
        /// </summary>
        /// <param name="SortCaps">Associated State Variable: SortCapabilities</param>
        /// <remarks>
        /// This REQUIRED action returns a CSV list of property names that can be used in the sortCriteria argument.
        /// 
        /// When it connects to the ContentDirectory service, the control point determines which properties can be
        /// used as sort criteria in a Browse() or Search() request. It does this via the GetSortCapabilities action:
        /// 
        /// Request:
        /// GetSortCapabilities()
        /// Response:
        /// GetSortCapabilities("dc:title,dc:creator,dc:date,res@size")
        /// </remarks>
        public override void GetSortCapabilities(out System.String SortCaps)
        {
            this.log("Not implemented: ContentDirectory.GetSortCapabilities()");
            SortCaps = "";
        }

        /// <summary>
        /// Action: GetSystemUpdateID
        /// TODO: This could be implemented using http://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.aspx
        /// </summary>
        /// <param name="Id">Associated State Variable: SystemUpdateID</param>
        public override void GetSystemUpdateID(out System.UInt32 Id)
        {
            this.log("Not implemented: ContentDirectoryService_GetSystemUpdateID(" + ")");
            Id = 0;
        }

        /// <summary>
        /// Action: UpdateObject
        /// </summary>
        /// <param name="ObjectID">Associated State Variable: A_ARG_TYPE_ObjectID</param>
        /// <param name="CurrentTagValue">Associated State Variable: A_ARG_TYPE_TagValueList</param>
        /// <param name="NewTagValue">Associated State Variable: A_ARG_TYPE_TagValueList</param>
        public override void UpdateObject(System.String ObjectID, System.String CurrentTagValue, System.String NewTagValue)
        {
            this.log("Not implemented: ContentDirectoryService_UpdateObject(" + ObjectID.ToString() + CurrentTagValue.ToString() + NewTagValue.ToString() + ")");
        }

        /// <summary>
        /// Action: X_GetRemoteSharingStatus
        /// </summary>
        /// <param name="Status">Associated State Variable: X_RemoteSharingEnabled</param>
        public override void X_GetRemoteSharingStatus(out System.Boolean Status)
        {
            Status = false;
            this.log("Not implemented: ContentDirectoryService_X_GetRemoteSharingStatus(" + ")");
        }
    }
}
